﻿/*
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.ComponentModel;
using System.Globalization;

namespace IvyOrm
{
    /// <summary>
    /// Provides conversion functions from a sourceType to a destinationType
    /// </summary>
    [ImmutableObject(true)]
    public class ValueConverterFactory
    {
        /// <summary>
        /// Returns the global default converter factory.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
        public static readonly ValueConverterFactory Default = new ValueConverterFactory();

        /// <summary>
        /// Provides a converter that handles objects of sourceType and returns objects of destinationType.
        /// </summary>
        /// <returns>True if converter is available, False if converter is not available</returns>
        /// <exception cref="System.ArgumentNullException" />
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        public virtual bool TryGetConverter(Type sourceType, Type destinationType, out Converter<object, object> converter)
        {
            if (sourceType == null)
                throw new ArgumentNullException("sourceType");
            if (destinationType == null)
                throw new ArgumentNullException("destinationType");

            //Checks if the destination is compatible without conversion
            if (destinationType.IsAssignableFrom(sourceType))
            {
                converter = (o) => o;
                return true;
            }

            if (destinationType == typeof(string))
            {
                converter = (v) => string.Format(CultureInfo.InvariantCulture, "{0}", v);
                return true;
            }

            //Nullable
            {
                var sourceUnderlyingType = Nullable.GetUnderlyingType(sourceType);
                var destinationUnderlyingType = Nullable.GetUnderlyingType(destinationType);
                if (sourceUnderlyingType != null && destinationUnderlyingType != null)
                {
                    //Both source and destination are nullable, will see if there is a underlying converter
                    Converter<object, object> underlyingTypeConverter;
                    if (TryGetConverter(sourceUnderlyingType, destinationUnderlyingType, out underlyingTypeConverter))
                    {
                        converter = (o) =>
                        {
                            if (o == null)
                                return null;

                            return underlyingTypeConverter(o);
                        };
                        return true;
                    }
                }
                else if (sourceUnderlyingType != null)
                {
                    //Only source is nullable
                    Converter<object, object> underlyingTypeConverter;
                    if (TryGetConverter(sourceUnderlyingType, destinationType, out underlyingTypeConverter))
                    {
                        converter = (o) =>
                        {
                            if (o == null)
                                throw new InvalidCastException("Unable to convert null to " + destinationType.FullName);

                            return underlyingTypeConverter(o);
                        };
                        return true;
                    }
                }
                else if (destinationUnderlyingType != null)
                {
                    //Only the destination is nullable
                    Converter<object, object> underlyingTypeConverter;
                    if (TryGetConverter(sourceType, destinationUnderlyingType, out underlyingTypeConverter))
                    {
                        converter = (o) =>
                        {
                            if (o == null)
                                return null;

                            return underlyingTypeConverter(o);
                        };
                        return true;
                    }
                }
            }




            //Looks for a Convert.ToType method, ex. Convert.ToInt(..), Convert.ToDouble(..)
            {
                var toMethod = typeof(Convert).GetMethod("To" + destinationType.Name, new Type[] { sourceType });
                if (toMethod != null && toMethod.ReturnType == destinationType && toMethod.GetParameters()[0].ParameterType != typeof(object))
                {
                    converter = (v) => toMethod.Invoke(null, new object[] { v });
                    return true;
                }
            }

            //To Enum
            if (destinationType.IsEnum)
            {
                if (sourceType == typeof(string))
                {
                    converter = (o) => Enum.Parse(destinationType, (string)o);
                    return true;
                }

                Converter<object, object> underlyingTypeConverter;
                var underlyingType = Enum.GetUnderlyingType(destinationType);
                if (TryGetConverter(sourceType, underlyingType, out underlyingTypeConverter))
                {
                    var toMethod = typeof(Enum).GetMethod("ToObject",
                                                            new Type[] { typeof(Type), underlyingType });

                    converter = (o) =>
                    {
                        var underlyingValue = underlyingTypeConverter(o);
                        return toMethod.Invoke(null, new object[] { destinationType, underlyingValue });
                    };
                    return true;
                }
            }

            //From Enum
            if (sourceType.IsEnum)
            {
                Debug.Assert(destinationType != typeof(string)); //Should have been handled earlier in the function

                Converter<object, object> underlyingTypeConverter;
                var underlyingType = Enum.GetUnderlyingType(sourceType);
                if (TryGetConverter(underlyingType, destinationType, out underlyingTypeConverter))
                {
                    converter = (o) =>
                    {
                        var underlyingValue = Convert.ChangeType(o, underlyingType, CultureInfo.InvariantCulture);
                        return underlyingTypeConverter(underlyingValue);
                    };
                    return true;
                }
            }

            //TypeDescriptor.GetConverter
            if (sourceType == typeof(string))
            {
                var typeConverter = TypeDescriptor.GetConverter(destinationType);
                if (typeConverter.CanConvertFrom(sourceType))
                {
                    converter = (v) => typeConverter.ConvertFromString((string)v);
                    return true;
                }
            }

            if (sourceType.IsAssignableFrom(destinationType))
            {
                converter = (v) =>
                {
                    if (!destinationType.IsInstanceOfType(v))
                        throw new InvalidCastException(string.Format(CultureInfo.InvariantCulture, "Unable to convert {0} to {1}", v, destinationType));

                    return v;
                };
                return true;
            }

            //No Converter Found
            converter = null;
            return false;
        }


        /// <summary>
        /// Returns a converter from the sourceType to the destinationType to be used when converting from a .net object to the database. 
        /// Throws a NotSupportedException if converter is not found.
        /// </summary>
        /// <exception cref="System.ArgumentNullException" />
        /// <exception cref="System.NotSupportedException" />
        public virtual Converter<object, object> GetToDatabaseConverter(Type sourceType, Type destinationType)
        {
            return GetConverter(sourceType, destinationType);
        }

        /// <summary>
        /// Returns a converter from the sourceType to the destinationType to be used when converting from a database result set to a .net object. 
        /// Throws a NotSupportedException if converter is not found.
        /// </summary>
        /// <exception cref="System.ArgumentNullException" />
        /// <exception cref="System.NotSupportedException" />
        public virtual Converter<object, object> GetFromDatabaseConverter(Type sourceType, Type destinationType)
        {
            return GetConverter(sourceType, destinationType); 
        }


        /// <summary>
        /// Returns a converter from the sourceType to the destinationType.
        /// Throws a NotSupportedException if converter is not found.
        /// </summary>
        /// <exception cref="System.ArgumentNullException" />
        /// <exception cref="System.NotSupportedException" />
        public virtual Converter<object, object> GetConverter(Type sourceType, Type destinationType)
        {
            if (sourceType == null)
                throw new ArgumentNullException("sourceType");
            if (destinationType == null)
                throw new ArgumentNullException("destinationType");

            Converter<object, object> converter;
            if (!TryGetConverter(sourceType, destinationType, out converter) || converter == null)
                throw new NotSupportedException("Converter not available for " + sourceType.FullName + " to " + destinationType.FullName + " conversion");

            return converter;
        }
    }
}
